﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace XSerialization
{
    /// <summary>
    /// Serializes and deserializes objects into and from XML.
    /// </summary>
    public class XSerializer : IXSerializer
    {
        protected readonly ConcurrentDictionary<Type, IList<XProperty>> TypeXProperties;

        /// <summary>
        /// Ctor
        /// </summary>
        public XSerializer()
        {
            TypeXProperties = new ConcurrentDictionary<Type, IList<XProperty>>();
        }

        /// <summary>
        /// Deserializes the XML contained by the specified file.
        /// </summary>
        /// <param name="document">The File that contains the XML to deserialize.</param>
        /// <param name="type">The Type of the object that this XSerializer can deserialize.</param>
        /// <returns>The Object being deserialized.</returns>
        public object Deserialize(string document, Type type)
        {
            if (string.IsNullOrEmpty(document)) throw new ArgumentNullException("document");
            XDocument xmlDocument = XDocument.Load(document);
            return Deserialize(xmlDocument, type);
        }

        /// <summary>
        /// Deserializes the XML contained by the specified XDocument.
        /// </summary>
        /// <param name="document">The XDocument that contains the XML to deserialize.</param>
        /// <param name="type">The Type of the object that this XSerializer can deserialize.</param>
        /// <returns>The Object being deserialized.</returns>
        public object Deserialize(XDocument document, Type type)
        {
            if (document == null) throw new ArgumentNullException("document");
            return DeserializeElement(document.Root, type);
        }

        /// <summary>
        /// Deserializes the XML contained by the specified XDocument.
        /// </summary>
        /// <param name="document">The XDocument that contains the XML to deserialize.</param>
        /// <param name="o">The instance of the object that this XSerializer can deserialize. Only for the root Object and overrides all xml binding properties.</param>
        /// <returns>The Object being deserialized.</returns>
        public object Deserialize(XDocument document, object o)
        {
            if (document == null) throw new ArgumentNullException("document");
            if (o == null) throw new ArgumentNullException("o");

            return DeserializeElement(document.Root, o.GetType(), o);
        }

        /// <summary>
        /// Deserializes the XML contained by the specified XElement.
        /// </summary>
        /// <param name="element">The XElement that contains the XML to deserialize.</param>
        /// <param name="type">The Type of the object that this XSerializer can deserialize.</param>
        /// <param name="o">The instance of the object that this XSerializer can deserialize, optional</param>
        /// <returns>The Object being deserialized.</returns>
        public object DeserializeElement(XElement element, Type type, object o = null)
        {
            if (element == null) throw new ArgumentNullException("element");
            if (type == null) throw new ArgumentNullException("type");

            //create instance of the type if parameter is null
            object newObject = o ?? Activator.CreateInstance(type);

            foreach (XProperty property in GetXProperties(type))
            {
                XObject xObject = Utilities.GetXObjectFromElement(element, property.Binding.XmlName, property.Binding.IsAttribute);
                if (xObject == null) continue;

                string assingableValue = Utilities.GetXObjectValue(xObject, property.Binding.IsAttribute);

                //Simple type
                if (Utilities.IsSimpleType(property.Info.PropertyType))
                {
                    property.Info.SetValue(newObject, Utilities.ConvertXValueToObject(assingableValue, property.Info.PropertyType), null);
                    continue;
                }

                //List Type
                if (Utilities.IsListType(property.Info.PropertyType))
                {
                    //try to cast property to IList
                    IList list = (IList)property.Info.GetValue(newObject, null);
                    if (list == null) continue;

                    if (property.Binding.ObjectType == null) property.Binding.ObjectType = Utilities.GetListType(property.Info.PropertyType);

                    //preparing parallel working 
                    //IList<Task<object>> tasks = new List<Task<object>>();
                    foreach (XElement xElement in element.Elements())
                    {
                        if (xElement.Name.LocalName != property.Binding.XmlName) continue;

                        object elementObject = null;
                        if (Utilities.IsSimpleType(property.Binding.ObjectType))
                        {
                            //tasks.Add(Task<object>.Factory.StartNew(() => Utilities.ConvertXValueToObject(xElement.Value, property.Binding.ObjectType)));
                            elementObject = Utilities.ConvertXValueToObject(xElement.Value, property.Binding.ObjectType);
                        }
                        else
                        {
                            //tasks.Add(Task<object>.Factory.StartNew(() => DeserializeElement(xElement, property.Binding.ObjectType)));
                            elementObject = DeserializeElement(xElement, property.Binding.ObjectType);
                        }

                        list.Add(elementObject);
                    }
                    /*//start parallel working
                    Task.WaitAll(tasks.ToArray());
                    //add results
                    foreach (Task<object> task in tasks)
                    {
                        list.Add(task.Result);
                    }*/

                    continue;
                }

                //Complex type
                if (property.Binding.ObjectType == null) property.Binding.ObjectType = property.Info.PropertyType;

                Object propertyObject = DeserializeElement(xObject as XElement, property.Binding.ObjectType);
                property.Info.SetValue(newObject, propertyObject, null);
            }

            return newObject;
        }

        /// <summary>
        /// Serializes the specified Object and writes the XML to a file.
        /// </summary>
        /// <param name="o">The object to serialize.</param>
        /// <param name="rootName">XML root node name.</param>
        /// <param name="fileName">The file used to write the XML.</param>
        public void Serialize(object o, string rootName, string fileName)
        {
            if (string.IsNullOrEmpty(fileName)) throw new ArgumentNullException("fileName");

            XDocument xmlDocument = Serialize(o, rootName);
            xmlDocument.Save(fileName);
        }

        /// <summary>
        /// Serializes the specified Object and writes the XML to a XDocument.
        /// </summary>
        /// <param name="o">The object to serialize.</param>
        /// <param name="rootName">XML root node name.</param>
        public XDocument Serialize(object o, string rootName)
        {
            XDocument newDocument = new XDocument();
            newDocument.Add(SerializeElement(o, rootName));
            return newDocument;
        }

        /// <summary>
        /// Serializes the specified Object and writes the XML to a XElement.
        /// </summary>
        /// <param name="o">The object to serialize.</param>
        /// <param name="rootName">XML root node name.</param>
        public XElement SerializeElement(object o, string rootName)
        {
            if (o == null) throw new ArgumentNullException("o");
            if (string.IsNullOrEmpty(rootName)) throw new ArgumentNullException("rootName");

            XElement xElement = new XElement(rootName);
            Type type = o.GetType();

            foreach (XProperty property in GetXProperties(type))
            {
                object propertyObject = property.Info.GetValue(o, null);
                if (propertyObject == null && !property.Binding.IsRequired) continue;

                if (Utilities.IsSimpleType(property.Info.PropertyType))
                {
                    if (string.IsNullOrEmpty(property.Binding.XmlName))
                    {
                        xElement.Value = Utilities.ConvertObjectToXValue(propertyObject);
                        continue;
                    }

                    xElement.Add(CreateSimpleXObject(property.Binding.XmlName, propertyObject, property.Binding.IsAttribute));
                    continue;
                }

                if (Utilities.IsListType(property.Info.PropertyType))
                {
                    if (propertyObject == null) continue;
                    IEnumerable list = propertyObject as IEnumerable;
                    if (list == null) continue;

                    if (property.Binding.ObjectType == null) property.Binding.ObjectType = Utilities.GetListType(property.Info.PropertyType);

                    foreach (object listElement in list)
                    {
                        if (Utilities.IsSimpleType(property.Binding.ObjectType))
                        {
                            xElement.Add(CreateSimpleXObject(property.Binding.XmlName, listElement, property.Binding.IsAttribute));
                            continue;
                        }

                        xElement.Add(SerializeElement(listElement, property.Binding.XmlName));
                    }

                    continue;
                }

                if (property.Binding.ObjectType == null) property.Binding.ObjectType = property.Info.PropertyType;

                if (propertyObject == null) xElement.Add(CreateSimpleXObject(property.Binding.XmlName, null, false));
                else xElement.Add(SerializeElement(propertyObject, property.Binding.XmlName));
            }

            return xElement;
        }

        private object CreateSimpleXObject(string name, object value, bool isAttribute)
        {
            if (value == null) value = string.Empty;
            else value = Utilities.ConvertObjectToXValue(value);

            if (isAttribute) return new XAttribute(name, value);
            return new XElement(name, value);
        }

        private IEnumerable<XProperty> GetXProperties(Type type)
        {
            //get all properties with XmlBinding attribute
            if (!TypeXProperties.ContainsKey(type))
            {
                IList<XProperty> newXProperties = Utilities.GetXPropertiesFromType(type);
                TypeXProperties.TryAdd(type, newXProperties);
            }

            return TypeXProperties[type];
        }
    }
}
